# Replication File for Berk, Nicolai. 2025. "The Effect of Persuasive Communication on Voting Intentions", Political Behavior.

## 0. Setup

### Define working environment
library(here)
library(tidyverse)

### Get packages with appropriate version
renv::install()


## 1. Prepare data for analysis
source("functions/clean.R")

## 2. Load data
main_long <- read.csv(here("data/main_long_clean.csv"))

## 3. Replication of Main Analyses

### Framing Effect on Attitudes
frame_coef <- lm(attitude ~ frame, main_long)$coefficients[["framepro"]]
frame_cis <- confint(lm(attitude ~ frame, main_long))["framepro",]
attitude_sd <- sd(main_long$attitude[main_long$frame == "con"])
cohens_d <- frame_coef/attitude_sd
cohens_d_cis <- frame_cis/attitude_sd

m_all <- lm(attitude ~ frame, main_long)
m_wealth <- lm(attitude ~ frame, main_long %>% filter(issue == "waelth"))
m_tempo <- lm(attitude ~ frame, main_long %>% filter(issue == "tempo"))
m_asylum <- lm(attitude ~ frame, main_long %>% filter(issue == "asylum"))
m_education <- lm(attitude ~ frame, main_long %>% filter(issue == "education"))
m_pension <- lm(attitude ~ frame, main_long %>% filter(issue == "pension"))
m_post <- lm(att_t2 ~ frame, main_long)

beta_asylum <- m_asylum$coefficients[["framepro"]]
cohens_d_asylum <- beta_asylum/sd(main_long$attitude[main_long$issue == "asylum"])

beta_pension <- m_pension$coefficients[["framepro"]]
cohens_d_pension <- beta_pension/sd(main_long$attitude[main_long$issue == "pension"])

modelsummary::modelsummary(
    list(
        "All" = m_all,
        "Wealth" = m_wealth,
        "Speed" = m_tempo,
        "Asylum" = m_asylum,
        "Education" = m_education,
        "Pension" = m_pension,
        "Post-Info" = m_post
    ),
    stars = T,
    gof_map = c("nobs", "r.squared"),
    coef_map = 
    c(
        "framepro" = "Pro Frame"
        ),
    output = here("output/table_2.txt")
)

print(paste0(
    "On average, respondents were moved ",
    round(frame_coef, 2),
    " [", round(frame_cis[1], 2), ", ", round(frame_cis[2], 2),
    "] points on the 100-point scale. This corresponds to a Cohen's $d$ of ",
    round(cohens_d, 2),
    " [", round(cohens_d_cis[1], 2), ", ", round(cohens_d_cis[2], 2),
    "], a moderate effect and less than half of the pre-registered effect size of $d=0.5$."
))

#### Plot
main_long %>% 
    group_by(frame, party_pos_inparty) %>% 
    summarise(
        mean_ptv = mean(attitude),
        upper_ci = mean_ptv + qt(0.975,df=n()-1)*sd(attitude, na.rm = T)/sqrt(n()),
        lower_ci = mean_ptv - qt(0.975,df=n()-1)*sd(attitude, na.rm = T)/sqrt(n()),
    ) %>%
    mutate(
        frame = ifelse(frame == "pro", "Positive", "Negative")
    ) %>%
    ggplot(
        aes(
            x = frame,
            y = mean_ptv,
            ymin = lower_ci,
            ymax = upper_ci,
            col = party_pos_inparty,
            lty = party_pos_inparty,
            shape = party_pos_inparty,
            group = paste(party_pos_inparty)
            )
        ) +
    geom_pointrange() +
    geom_line() +
    geom_hline(yintercept = 0) +
    theme_minimal() +
    scale_color_manual(
        name = "Inparty Position",
        labels = c("Support", "Oppose"),
        values = c("#1ba31b", "#801414"),
        breaks = c("pro", "con")
    ) +
    scale_linetype_manual(
        name = "Inparty Position",
        values = c(1, 2),
        labels = c("Support", "Oppose"),
        breaks = c("pro", "con"),
        aesthetics = c("linetype", "shape")
    ) +
    ylab("Issue Attitude") +
    xlab("Frame Condition")

ggsave(here("output/figure_3.png"), width = 10, height = 6)

### Party Information
ppp_data <- 
    main_long %>%
    dplyr::select(id, party_pos, partytree, inpartytree, topic, contains("pp.")) %>%
    pivot_longer(
        cols = contains("pp."),
        names_to = c("issue", "party_eval"),
        names_pattern = "(.*)pp\\.(.*)\\.",
        values_to = "ppp"
    ) %>%
    filter(!is.na(ppp)) %>%
    mutate(party_received = partytree == party_eval)

m1 <- fixest::feglm(ppp ~ party_pos*party_received + inpartytree | as.factor(partytree), ppp_data, cluster = ppp_data$id)
m2 <- fixest::feglm(ppp ~ party_received + inpartytree | as.factor(partytree), ppp_data[ppp_data$party_pos == "pro",], cluster = ppp_data$id[ppp_data$party_pos == "pro"])
m3 <- fixest::feglm(ppp ~ party_received + inpartytree | as.factor(partytree), ppp_data[ppp_data$party_pos == "con",], cluster = ppp_data$id[ppp_data$party_pos == "con"])
m4 <- fixest::feglm(ppp ~ party_pos*party_received, ppp_data, cluster = ppp_data$id)


beta_info <- m1$coefficients["party_pospro:party_receivedTRUE"]

print(
    paste0(
        "Respondents clearly react to the treatment, placing parties as ", 
        round(beta_info, 2), 
        "points more supportive of the policy (100-point scale) when they learn that a party supports the policy, compared to receiving information about an opposing party's position (see interaction term first column)."))

modelsummary::modelsummary(
    list(
        "Interaction" = m4, 
        "Interaction, Controls" = m1,
        "Pro-Parties" = m2, 
        "Con-Parties" = m3),
    stars = T,
    gof_map = c("nobs", "r.squared", "vcov.type", "FE.*"),
    coef_map = c("party_pospro" = "Supportive Party", "party_receivedTRUE" = "Received Info", "party_pospro:party_receivedTRUE" = "Support X Received", "inpartytreeTRUE" = "Inparty"),
    output = "output/table_3.txt"
    )

### Framing Effects on Propensity to Vote
m0 <- lm(ptv_treated ~ frame_to_party, main_long)
m0_ctrl <- lm(ptv_treated ~ frame_to_party + inpartytree, main_long)
m1 <- lm(ptv_treated ~ frame_to_party*inpartytree, main_long)
m2 <- lm(ptv_treated ~ frame_to_party*inpartytree, main_long  %>% filter(partyclose.z. == "Nein"))

simple_coef_frame <- m0$coefficients[["frame_to_partyTRUE"]]
simple_ci_frame <- confint(m0)["frame_to_partyTRUE",]

controls_coef_frame <- m0_ctrl$coefficients[["frame_to_partyTRUE"]]
controls_coef_inparty <- m0_ctrl$coefficients[["inpartytreeTRUE"]]

modelsummary::modelsummary(
    list(
        "Simple" = m0,
        "Controls" = m0_ctrl,
        "Interaction" = m1,
        "Partisans" = m2
        ),
        stars = T,
    gof_map = c("nobs", "r.squared", "vcov.type", "FE: partytree", "FE: inpartytree", "FE: topic"),
    coef_map = c(
        "frame_to_partyTRUE" = "Frame Supports Party",
        "inpartytreeTRUE" = "Inparty",
        "frame_to_partyTRUE:inpartytreeTRUE" = "Frame X Inparty"),
    output = "output/table_4.txt"
    )

#### Plot
sd_control <- sd(main_long$ptv_treated[main_long$frame_to_party == F])
ees <- 0.2 * sd_control
sesoi <- 0.1 * sd_control

modelsummary::modelplot(
    list(
        "Partisans Only" = m2,
        "Controls" = m0_ctrl,
        "Interaction" = m1,
        "Simple" = m0
        ),
    coef_map = c(
        "frame_to_partyTRUE" = "Frame Supports Party"
    )
    ) +
    geom_vline(xintercept = 0) +
    geom_vline(xintercept = ees, linetype = 3) +
    geom_label(aes(x = ees, y = 0.5, label = "Expected Effect"), hjust = .5, vjust = 0) +
    geom_vline(xintercept = -ees, linetype = 3) +
    geom_label(aes(x = -ees, y = 0.5, label = "Expected Effect"), hjust = .5, vjust = 0) +
    geom_vline(xintercept = sesoi, linetype = 2) +
    geom_label(aes(x = sesoi, y = 0.5, label = "SESOI"), hjust = .5, vjust = 0) +
    geom_vline(xintercept = -sesoi, linetype = 2) +
    geom_label(aes(x = -sesoi, y = 0.5, label = "SESOI"), hjust = .5, vjust = 0) +
    xlim(-10, 10) +
    theme_minimal() +
    labs(color = "Model") +
    theme(
        axis.text.y = element_blank(),
        axis.ticks = element_blank()
    ) +
    scale_color_discrete(
        breaks = c("Simple", "Controls", "Interaction", "Partisans Only")
        )

ggsave(here("output/figure_4.png"), width = 10, height = 5)

print(
    paste0(
        "Compared to a frame opposing the party's condition, a supportive framing increases the propensity to vote by ",
        round(simple_coef_frame, 2),
        " [", round(simple_ci_frame[1], 2), ", ", 
        round(simple_ci_frame[2], 2), "] points on the 100-point scale."
    )
)

print(
    paste0(
        "The effect becomes negative here, while partisanship has a strong and highly significant impact on party support (",
        round(controls_coef_inparty, 2),
        " on 100-point scale)."
    )
)

### Cue-Taking Effects

## model those receiving inparty positions versus those that do not
m1_cue <- lm(delta_att ~ inpartytree * party_pos_inparty, main_long)
m1p_cue <- lm(delta_att ~ inpartytree * party_pos_inparty, main_long %>% filter(partyclose.z. == "Nein"))
m1ip_cue <- lm(delta_att ~ party_pos_inparty, main_long %>% filter(inpartytree == T))
m1op_cue <- lm(delta_att ~ party_pos_inparty, main_long %>% filter(inpartytree == F))

modelsummary::modelsummary(
    list(
        "Direct" = m1_cue,
        "Partisans" = m1p_cue,
        "Inparty" = m1ip_cue,
        "Outparty" = m1op_cue
    ),
    stars = T,
    gof_map = c("r.squared", "nobs", "FE: inparty", "FE: topic", "FE: frame"),
    coef_map = 
    c(
        "inpartytreeTRUE" = "Inparty Shown", 
        "party_pos_inpartypro" = "Inparty Pro Policy",
        "inpartytreeTRUE:party_pos_inpartypro" = "Inparty Pro Policy X Inparty",
        "party_pospro" = "Party Position",
        "inpartytreeTRUE:party_pospro" = "Party. Pos. X Inparty"
        ),
    escape = F,
    output = here("output/table_5.txt")
    )

pretreatment_diff <- 
    main_long %>%
    group_by(party_pos_inparty) %>%
    summarise(
        mean_att = mean(attitude)
        ) %>% 
    pull(mean_att) %>% 
    diff()

print(
    paste0(
        "Nevertheless, receiving a supporting inparty position increased support of the policy by ",
        round(m1_cue$coefficient["inpartytreeTRUE:party_pos_inpartypro"], 2),
        " points on a 100-point scale (Cohen's $d$ = ", 
        round(m1_cue$coefficient["inpartytreeTRUE:party_pos_inpartypro"]/sd(main_long$delta_att[!main_long$inpartytree], na.rm = T), 2),
        "), compared to the outparty condition. Despite strong priming through the first attitudinal question, some cue-taking takes place. The effect is small, but highly significant. Restricting the model to only partisans, the effect is even stronger, around ",
        round(m1p_cue$coefficient["inpartytreeTRUE:party_pos_inpartypro"], 2), 
        " points on a 100-point scale (Cohen's $d$ = ",
        round(m1p_cue$coefficient["inpartytreeTRUE:party_pos_inpartypro"]/sd(main_long$delta_att[!main_long$inpartytree], na.rm = T), 2), 
        ")."
    )
)

print(
    paste0(
        "Interestingly, partisans also learned from out-parties' positions: when shown an outparty's position, respondents move ",
        round(m1op_cue$coefficient["party_pospro"], 2),
        " closer toward the in-party's position."
    )
)

#### Plot
main_long %>% 
    group_by(inpartytree, party_pos_inparty) %>% 
    summarise(
        mean_pre = mean(attitude),
        upper_ci_pre = mean_pre + qt(0.975,df=n()-1)*sd(attitude, na.rm = T)/sqrt(n()),
        lower_ci_pre = mean_pre - qt(0.975,df=n()-1)*sd(attitude, na.rm = T)/sqrt(n()),
        mean_post = mean(att_t2, na.rm = T),
        upper_ci_post = mean_post + qt(0.975,df=n()-1)*sd(att_t2, na.rm = T)/sqrt(n()),
        lower_ci_post = mean_post - qt(0.975,df=n()-1)*sd(att_t2, na.rm = T)/sqrt(n())
    ) %>%
    pivot_longer(
        cols = contains("pre") | contains("post"),
        names_pattern = "(.*)_(.*)",
        names_to = c("attitude", "time"),
    ) %>%
    pivot_wider(
        names_from = c("attitude"),
        values_from = c("value")
    ) %>% 
    mutate(
        time = ifelse(time == "pre", "Pre", "Post"),
        time = factor(time, levels = c("Pre", "Post")),
        inpartytree = ifelse(inpartytree, "Inparty", "Outparty")
    ) %>% 
    ggplot(
        aes(
            x = time,
            y = mean,
            ymin = lower_ci,
            ymax = upper_ci,
            col = party_pos_inparty,
            shape = party_pos_inparty,
            lty = party_pos_inparty,
            group = paste(inpartytree, party_pos_inparty)
            )
        ) +
    geom_pointrange() +
    geom_line() +
    theme_minimal() +
    facet_wrap( ~ inpartytree) +
    scale_color_manual(
        name = "Inparty Position",
        labels = c("Support", "Oppose"),
        values = c("#1ba31b", "#801414"),
        breaks = c("pro", "con")
    ) +
    scale_linetype_manual(
        name = "Inparty Position",
        values = c(1, 2),
        labels = c("Support", "Oppose"),
        breaks = c("pro", "con"),
        aesthetics = c("linetype", "shape")
    ) +
    xlab("") + ylab("Issue Attitude")

ggsave(here("output/figure_5.png"), width = 10, height = 6)

### Examining the Party vs. Policy Trade-off
m1_ppto <- lm(partyvote ~ frame_to_party*delta_att_inparty, main_long %>% filter(inpartytree == TRUE))
m2_ppto <- lm(ptv_treated ~ frame_to_party*delta_att_inparty, main_long %>% filter(inpartytree == TRUE))

modelsummary::modelsummary(
    list(
        "DV: Vote" = m1_ppto,
        "DV: PTV" = m2_ppto
    ),
    stars = T,
    gof_map = c("r.squared", "nobs", "FE: inparty", "FE: topic", "FE: frame"),
    coef_map = 
    c(
        "frame_to_partyTRUE" = "Frame Supports Party",
        "delta_att_inparty" = "$\\Delta$ Attitude",
        "frame_to_partyTRUE:delta_att_inparty" = "Frame X $\\Delta$ Attitude"
        ),
    output = here("output/table_6.txt"),
    escape = F
    )


### Plot
inter <- interflex::interflex(
    Y = "partyvote",
    D = "frame_to_party",
    X = "delta_att_inparty",
    data = 
        main_long %>% 
        filter(!is.na(partyvote), !is.na(delta_att_inparty), inpartytree == T) %>% 
        as.data.frame(), 
    estimator = "binning",
    Ylabel = "Vote Intention", 
    Dlabel = "Frame", 
    Xlabel = "Attitude Change towards Party Position")

plot(
    inter, 
    xlim = c(-50, 50),
    theme.bw = T
    )

ggsave(here("output/figure_6.png"), width = 10, height = 6)
